<?php

namespace Encore\Admin\Console;

use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use Symfony\Component\Finder\Finder;

class TestCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'admin:test {controllerDir}  {--all} {--name=}  {--force}';


    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Make a service';

    public function handle()
    {
        //variables
        $controllerDir = $this->argument('controllerDir');
        $controllerPath = app_path('Http/Controllers/' . $controllerDir);
        $basePath = 'App/' . Str::after($controllerPath, app_path());
        $name = $this->option('name');
        $all = $this->hasOption('all');
        $force = boolval($this->option('force'));

        //judge that name or all must be set one of them.
        if (!$name && !$all) {
            $this->error('The --name=controllerName or --all must be set one of them');
            return;
        }

        //if name exists, then judge that the controller is exists.
        if ($name && !$this->controllerExists($controllerDir, $name)) {
            $this->error(sprintf('The controller "%s.php" is not exists in %s directory', $name, $basePath));
            return;
        }

        //judge the tests/feature directory can be written.
        if (!$this->judgeDirCanBeWritten(base_path('tests/Feature'))) {
            $this->error('The tests/Feature directory can not be written.');
            return;
        }


        //judge the controller directory is exists.
        if (!is_dir(app_path('Http/Controllers/' . $controllerDir))) {
            $this->error('The controller directory is not exists.');
            return;
        }

        //store test files directory
        $testDir = base_path('tests/Feature/' . $controllerDir);

        //if the test directory is not exists, create it.
        if (!is_dir($testDir)) {
            (new Filesystem())->makeDirectory($testDir, 0755, true);
        }

        //class and methods
        $classMethods = $this->getClassAndMethods($controllerDir, $name);
        //if empty, return
        if (empty($classMethods)) {
            $this->error('The controller directory is empty.');
            return;
        }

        //generate test files
        $this->generateTestFiles($classMethods, $testDir, $controllerDir, $force);
    }

    /**
     * generate test files.
     * @param array $classMethods
     * @param string $testDir
     * @param string $controllerDir
     * @param bool $isForce
     * @return void
     */
    protected function generateTestFiles(array $classMethods, string $testDir, string $controllerDir, bool $isForce): void
    {
        foreach ($classMethods as $class => $methods) {
            $filename = $testDir . DIRECTORY_SEPARATOR . $class . 'Test.php';
            if (!file_exists($filename) || $isForce) {
                $dummyMethods = '';
                $stub = file_get_contents(__DIR__ . '/stubs/featureTest.stub');

                foreach ($methods as $method) {
                    $method = ucfirst($method);
                    $dummyMethods .= "    public function test{$method}()\n";
                    $dummyMethods .= "    {\n\n";
                    $dummyMethods .= "    }\n\n";
                }
                //replace the last \n from dummy functions.
                $dummyMethods = rtrim($dummyMethods, "\n");

                $stub = str_replace([
                    'DummyNamespace',
                    'DummyClass',
                    'DummyFunction',
                ], [
                    'Tests\Feature\\' . $controllerDir,
                    ($class . 'Test'),
                    $dummyMethods,
                ], $stub);
                file_put_contents($filename, $stub);

                $this->info(sprintf('The test file "%s" is created.', 'tests/' . Str::afterLast($filename, 'tests')));

            } else {
                $filename = 'tests/' . Str::afterLast($filename, 'tests');
                $this->error(sprintf('The test file "%s" is exists.you can use --force option to overwrite it.', $filename));
            }
        }
    }

    /**
     * get classname and methods name from the controller dir files.
     * @param string $controllerDir
     * @param $name
     * @return array [{className:methods}, ...]
     */
    protected function getClassAndMethods(string $controllerDir, $name): array
    {
        $namespace = app()->getNamespace();

        $files = (new Finder())
            ->notName('Controller.php')
            ->in(app_path('Http/Controllers/' . $controllerDir))
            ->files();

        $arr = [];
        foreach ($files as $resource) {
            $resource = $namespace . str_replace(
                    ['/', '.php'],
                    ['\\', ''],
                    Str::after($resource->getPathname(), app_path() . DIRECTORY_SEPARATOR)
                );

            //get all functions from resource.
            $methods = get_class_methods($resource);

            //except magic methods and special methods.
            $except = [
                'message', 'failed', 'internalError', 'created', 'respond', 'success',
                'notFond', 'getMiddleware', 'middleware', 'authorize', 'authorizeResource', 'toAdmin',
                'authorizeForUser', 'dispatchNow', 'dispatchSync', 'getStatusCode', 'setStatusCode',
                'pageData', 'validateWith', 'validate', 'validateWithBag', 'status', 'callAction',
            ];

            //filter the except methods.
            $methods = collect($methods)->filter(function ($function) {
                return !Str::startsWith($function, '__') || $function === '__invoke';
            })->diff($except)->values()->toArray();

            $arr[Str::before(Str::afterLast($resource, '\\'), 'Controller')] = $methods;
        }


        //only single controller to generate test file.
        if ($name) {
            $name = Str::beforeLast($name, 'Controller');
            $arr = [$name => $arr[$name]];
        }
        return $arr;
    }

    /**
     * judge if the directory is can be written.
     * @param string $dir
     * @return bool
     */
    protected function judgeDirCanBeWritten(string $dir): bool
    {
        $handle = @fopen($dir . '/test.txt', 'w');
        if ($handle) {
            unlink($dir . '/test.txt');
            return true;
        }
        return false;
    }

    /**
     * @param string $controllerDir
     * @param string $name
     * @return bool
     */
    protected function controllerExists(string $controllerDir, string $name): bool
    {
        return file_exists(app_path('Http/Controllers/' . $controllerDir . '/' . $name . '.php'));
    }
}
